/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package context

import (
	reqContext "context"
	"time"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/core"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/iam"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"github.com/pkg/errors"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain_sdk/metrics"
)

type Client struct {
	context.Providers
	iam.SigningIdentity
}

type Local struct {
	context.Client
	localDiscovery ichain.DiscoveryService
}

func (c *Local) LocalDiscoveryService() ichain.DiscoveryService {
	return c.localDiscovery
}

type Chain struct {
	context.Client
	chainService ichain.ChainService
	chainID      string
	metrics      *metrics.ClientMetrics
}

func (c *Chain) Providers() context.Client {
	return c
}

func (c *Chain) ChainService() ichain.ChainService {
	return c.chainService
}

func (c *Chain) ChainID() string {
	return c.chainID
}

// Provider 实现了providers/context Providers 接口
type Provider struct {
	cryptoSuiteConfig      core.CryptoSuiteConfig
	endpointConfig         ichain.EndpointConfig
	identityConfig         iam.IdentityConfig
	userStore              iam.UserStore
	cryptoSuite            core.CryptoSuite
	localDiscoveryProvider ichain.LocalDiscoveryProvider
	signingManager         core.SigningManager
	idMgmtProvider         iam.IdentityManagerProvider
	iChainProvider         ichain.IChainProvider
	chainProvider          ichain.ChainProvider
	clientMetrics          *metrics.ClientMetrics
}

func (c *Provider) CryptoSuite() core.CryptoSuite {
	return c.cryptoSuite
}

func (c *Provider) IdentityManager(orgName string) (iam.IdentityManager, bool) {
	return c.idMgmtProvider.IdentityManager(orgName)
}

func (c *Provider) SigningManager() core.SigningManager {
	return c.signingManager
}

func (c *Provider) UserStore() iam.UserStore {
	return c.userStore
}

func (c *Provider) IdentityConfig() iam.IdentityConfig {
	return c.identityConfig
}

func (c *Provider) LocalDiscoveryProvider() ichain.LocalDiscoveryProvider {
	return c.localDiscoveryProvider
}

func (c *Provider) ChainProvider() ichain.ChainProvider {
	return c.chainProvider
}

func (c *Provider) IChainProvider() ichain.IChainProvider {
	return c.iChainProvider
}

func (c *Provider) EndpointConfig() ichain.EndpointConfig {
	return c.endpointConfig
}

func (c *Provider) GetMetrics() *metrics.ClientMetrics {
	return c.clientMetrics
}

type SDKContextParams func(opts *Provider)

func WithCryptoSuiteConfig(cryptoSuiteConfig core.CryptoSuiteConfig) SDKContextParams {
	return func(ctx *Provider) {
		ctx.cryptoSuiteConfig = cryptoSuiteConfig
	}
}

func WithEndpointConfig(endpointConfig ichain.EndpointConfig) SDKContextParams {
	return func(ctx *Provider) {
		ctx.endpointConfig = endpointConfig
	}
}

func WithIdentityConfig(identityConfig iam.IdentityConfig) SDKContextParams {
	return func(ctx *Provider) {
		ctx.identityConfig = identityConfig
	}
}

func WithUserStore(userStore iam.UserStore) SDKContextParams {
	return func(ctx *Provider) {
		ctx.userStore = userStore
	}
}

func WithCryptoSuite(cryptoSuite core.CryptoSuite) SDKContextParams {
	return func(ctx *Provider) {
		ctx.cryptoSuite = cryptoSuite
	}
}

func WithLocalDiscoveryProvider(discoveryProvider ichain.LocalDiscoveryProvider) SDKContextParams {
	return func(ctx *Provider) {
		ctx.localDiscoveryProvider = discoveryProvider
	}
}

func WithSigningManager(signingManager core.SigningManager) SDKContextParams {
	return func(ctx *Provider) {
		ctx.signingManager = signingManager
	}
}

func WithIdentityManagerProvider(provider iam.IdentityManagerProvider) SDKContextParams {
	return func(ctx *Provider) {
		ctx.idMgmtProvider = provider
	}
}

func WithIChainProvider(infraProvider ichain.IChainProvider) SDKContextParams {
	return func(ctx *Provider) {
		ctx.iChainProvider = infraProvider
	}
}

func WithChainProvider(chainProvider ichain.ChainProvider) SDKContextParams {
	return func(ctx *Provider) {
		ctx.chainProvider = chainProvider
	}
}

func WithClientMetrics(cm *metrics.ClientMetrics) SDKContextParams {
	return func(ctx *Provider) {
		ctx.clientMetrics = cm
	}
}

func NewProvider(params ...SDKContextParams) *Provider {
	ctxProvider := Provider{}
	for _, param := range params {
		param(&ctxProvider)
	}
	return &ctxProvider
}

type localServiceInit interface {
	Initialize(context context.Local) error
}

func NewLocal(clientProvider context.ClientProvider) (*Local, error) {
	client, err := clientProvider()
	if err != nil {
		return nil, errors.WithMessage(err, "failed to get client context to create local context")
	}

	discoveryService, err := client.LocalDiscoveryProvider().CreateLocalDiscoveryService(client.Identifier().IAMID)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to create local discovery service")
	}

	local := &Local{
		Client:         client,
		localDiscovery: discoveryService,
	}

	if ci, ok := discoveryService.(localServiceInit); ok {
		if err := ci.Initialize(local); err != nil {
			return nil, err
		}
	}

	return local, nil
}

type serviceInit interface {
	Initialize(context context.Chain) error
}

func NewChain(clientProvider context.ClientProvider, chainID string) (*Chain, error) {

	client, err := clientProvider()
	if err != nil {
		return nil, errors.WithMessage(err, "failed to get client context to create chain client")
	}

	chainService, err := client.ChainProvider().ChainService(client, chainID)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to get chain service to create chain client")
	}

	chain := &Chain{
		Client:       client,
		chainService: chainService,
		chainID:      chainID,
		metrics:      client.GetMetrics(),
	}
	if pi, ok := chainService.(serviceInit); ok {
		if err := pi.Initialize(chain); err != nil {
			return nil, err
		}
	}
	return chain, nil
}

type reqContextKey string

// ReqContextTimeoutOverrides key for grpc context value of timeout overrides
var ReqContextTimeoutOverrides = reqContextKey("timeout-overrides")
var reqContextCommManager = reqContextKey("commManager")
var reqContextClient = reqContextKey("clientContext")

// WithTimeoutType sets timeout by type defined in config to request context
func WithTimeoutType(timeoutType ichain.TimeoutType) ReqContextOptions {
	return func(ctx *requestContextOpts) {
		ctx.timeoutType = timeoutType
	}
}

// WithTimeout sets timeout time duration to request context
func WithTimeout(timeout time.Duration) ReqContextOptions {
	return func(ctx *requestContextOpts) {
		ctx.timeout = timeout
	}
}

// WithParent sets existing reqContext as a parent ReqContext
func WithParent(context reqContext.Context) ReqContextOptions {
	return func(ctx *requestContextOpts) {
		ctx.parentContext = context
	}
}

// ReqContextOptions parameter for creating requestContext
type ReqContextOptions func(opts *requestContextOpts)

type requestContextOpts struct {
	timeoutType   ichain.TimeoutType
	timeout       time.Duration
	parentContext reqContext.Context
}

func NewRequest(client context.Client, options ...ReqContextOptions) (reqContext.Context, reqContext.CancelFunc) {

	//'-1' to get default config timeout when timeout options not passed
	reqCtxOpts := requestContextOpts{timeoutType: -1}
	for _, option := range options {
		option(&reqCtxOpts)
	}

	parentContext := reqCtxOpts.parentContext
	if parentContext == nil {
		//when parent request context not set, use background context
		parentContext = reqContext.Background()
	}

	var timeout time.Duration
	if reqCtxOpts.timeout > 0 {
		timeout = reqCtxOpts.timeout
	} else if timeoutOverride := requestTimeoutOverride(parentContext, reqCtxOpts.timeoutType); timeoutOverride > 0 {
		timeout = timeoutOverride
	} else {
		timeout = client.EndpointConfig().Timeout(reqCtxOpts.timeoutType)
	}

	ctx := reqContext.WithValue(parentContext, reqContextCommManager, client.IChainProvider().CommManager())
	ctx = reqContext.WithValue(ctx, reqContextClient, client)
	ctx, cancel := reqContext.WithTimeout(ctx, timeout)

	return ctx, cancel
}

// RequestCommManager extracts the CommManager from the request-scoped context.
func RequestCommManager(ctx reqContext.Context) (ichain.CommManager, bool) {
	commManager, ok := ctx.Value(reqContextCommManager).(ichain.CommManager)
	return commManager, ok
}

// RequestClientContext extracts the Client Context from the request-scoped context.
func RequestClientContext(ctx reqContext.Context) (context.Client, bool) {
	clientContext, ok := ctx.Value(reqContextClient).(context.Client)
	return clientContext, ok
}

// requestTimeoutOverrides extracts the timeout from timeout override map from the request-scoped context.
func requestTimeoutOverride(ctx reqContext.Context, timeoutType ichain.TimeoutType) time.Duration {
	timeoutOverrides, ok := ctx.Value(ReqContextTimeoutOverrides).(map[ichain.TimeoutType]time.Duration)
	if !ok {
		return 0
	}
	return timeoutOverrides[timeoutType]
}
